// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.restapi.change;

import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public class ListChangeCommentsTest {

  @Test
  public void commentsLinkedToChangeMessagesIgnoreGerritAutoGenTaggedMessages() {
    /* Comments should not be linked to Gerrit's autogenerated messages */
    List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
    List<ChangeMessage> changeMessages =
        createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");

    changeMessages.add(
        newChangeMessage("ignore", "cmAutoGenByGerrit", "15", ChangeMessagesUtil.TAG_MERGED));

    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);

    assertThat(getComment(comments, "c1").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
    /* comment 2 ignored the auto-generated message because it has a Gerrit tag */
    assertThat(getComment(comments, "c2").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm2").getKey().uuid());
    assertThat(getComment(comments, "c3").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());

    // Make sure no comment is linked to the auto-gen message
    Set<String> changeMessageIds =
        comments.stream().map(c -> c.changeMessageId).collect(Collectors.toSet());
    assertThat(changeMessageIds)
        .doesNotContain(getChangeMessage(changeMessages, "cmAutoGenByGerrit").getKey().uuid());
  }

  @Test
  public void commentsLinkedToCorrectAccounts() {

    List<CommentInfo> comments =
        createComments("c0", "10", "c1", "11", "c2", "11", "c3", "11", "c4", "11", "c5", "21");
    int accountId1 = 12345;
    int accountId2 = 12346;
    int accountId3 = 12347;
    linkAuthor(comments.get(0), accountId1);
    // comments 1-4 have same timestamp
    linkAuthor(comments.get(1), accountId2);
    linkAuthor(comments.get(2), accountId1);
    linkAuthor(comments.get(3), accountId1);
    linkAuthor(comments.get(4), accountId2);

    linkAuthor(comments.get(5), accountId1);

    // Change massages have exactly same timestamps
    ImmutableList<ChangeMessage> changeMessages =
        ImmutableList.of(
            createChangeMessage("cm0", "10", Account.id(accountId1)),
            createChangeMessage("cm1", "11", Account.id(accountId1)),
            createChangeMessage("cm2", "11", Account.id(accountId2)),
            // unrelated message by other account in-between
            createChangeMessage("cm2.2", "15", Account.id(accountId3)),
            createChangeMessage("cm3", "21", Account.id(accountId1)));

    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);

    assertThat(getComment(comments, "c0").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm0").getKey().uuid());

    // belongs to account2 -assigned to account2
    assertThat(getComment(comments, "c1").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm2").getKey().uuid());

    // belongs to account1 -assigned to account1
    assertThat(getComment(comments, "c2").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
    assertThat(getComment(comments, "c3").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());

    // belongs to account2 - assigned to account2
    assertThat(getComment(comments, "c4").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm2").getKey().uuid());

    // different timestamp - assigned to a different message
    assertThat(getComment(comments, "c5").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());

    // Make sure no comment is linked to the auto-gen message
    Set<String> changeMessageIds =
        comments.stream().map(c -> c.changeMessageId).collect(Collectors.toSet());
    assertThat(changeMessageIds)
        .doesNotContain(getChangeMessage(changeMessages, "cm2.2").getKey().uuid());
  }

  @Test
  public void commentsLinkedToCorrectAccountsIfUserNotMatched() {

    String tsCm0 = "10";
    String tsCm1 = "11";
    String tsCm2 = "12";
    List<CommentInfo> comments =
        createComments(
            "commentMessage0", tsCm0, "commentMessage1", tsCm1, "commentMessage2", tsCm2);
    int accountId1 = 1;
    int accountId2 = 2;
    int accountId2Imported = 0;
    int accountId3 = 3;
    linkAuthor(comments.get(0), accountId1);
    linkAuthor(comments.get(1), accountId2Imported);
    linkAuthor(comments.get(2), accountId3);

    ImmutableList<ChangeMessage> changeMessages =
        ImmutableList.of(
            createChangeMessage("changeMessage0", tsCm0, Account.id(accountId1)),
            createChangeMessage("changeMessage1", tsCm1, Account.id(accountId2)),
            createChangeMessage("changeMessage2", tsCm2, Account.id(accountId3)));

    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, false);

    assertThat(getComment(comments, "commentMessage0").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "changeMessage0").getKey().uuid());

    assertThat(getComment(comments, "commentMessage1").changeMessageId).isNull();

    assertThat(getComment(comments, "commentMessage2").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "changeMessage2").getKey().uuid());
  }

  @Test
  public void commentsLinkedToChangeMessagesAllowLinkingToAutoGenTaggedMessages() {
    /* Human comments are allowed to be linked to autogenerated messages */
    List<CommentInfo> comments = createComments("c1", "00", "c2", "10", "c3", "25");
    List<ChangeMessage> changeMessages =
        createChangeMessages("cm1", "00", "cm2", "16", "cm3", "30");

    changeMessages.add(
        newChangeMessage(
            "cmAutoGen", "cmAutoGen", "15", ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX));

    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);

    assertThat(getComment(comments, "c1").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm1").getKey().uuid());
    /* comment 2 did not ignore the auto-generated change message */
    assertThat(getComment(comments, "c2").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cmAutoGen").getKey().uuid());
    assertThat(getComment(comments, "c3").changeMessageId)
        .isEqualTo(getChangeMessage(changeMessages, "cm3").getKey().uuid());
  }

  /**
   * Create a list of comments from the specified args args should be passed as consecutive pairs of
   * messages and timestamps example: (m1, t1, m2, t2, ...)
   */
  private static List<CommentInfo> createComments(String... args) {
    List<CommentInfo> comments = new ArrayList<>();
    for (int i = 0; i < args.length; i += 2) {
      String message = args[i];
      String ts = args[i + 1];
      comments.add(newCommentInfo(message, ts));
    }
    return comments;
  }

  private void linkAuthor(CommentInfo commentInfo, int accountId) {
    commentInfo.author = new AccountInfo(accountId);
  }

  /**
   * Create a list of change messages from the specified args args should be passed as consecutive
   * pairs of messages and timestamps example: (m1, t1, m2, t2, ...). the tag parameter for the
   * created change messages will be null.
   */
  private static List<ChangeMessage> createChangeMessages(String... args) {
    List<ChangeMessage> changeMessages = new ArrayList<>();
    for (int i = 0; i < args.length; i += 2) {
      String message = args[i];
      String ts = args[i + 1];
      changeMessages.add(createChangeMessage(message, ts, Optional.empty()));
    }
    return changeMessages;
  }

  /** Creates a ChangeMessages with the specified author. */
  private static ChangeMessage createChangeMessage(String message, String ts, Account.Id author) {
    return createChangeMessage(message, ts, Optional.of(author));
  }

  private static ChangeMessage createChangeMessage(
      String message, String ts, Optional<Account.Id> author) {
    String key = message + "Key";
    return newChangeMessage(key, author, message, ts, null);
  }

  /** Create a new CommentInfo with a given message and timestamp */
  private static CommentInfo newCommentInfo(String message, String ts) {
    CommentInfo c = new CommentInfo();
    c.message = message;
    c.updated = Timestamp.valueOf("2000-01-01 00:00:" + ts);
    return c;
  }

  /** Create a new change message with an id, message, timestamp and tag */
  private static ChangeMessage newChangeMessage(String id, String message, String ts, String tag) {
    return newChangeMessage(id, Optional.empty(), message, ts, tag);
  }

  private static ChangeMessage newChangeMessage(
      String id, Optional<Account.Id> accountId, String message, String ts, String tag) {
    ChangeMessage.Key key = ChangeMessage.key(Change.id(1), id);
    Instant timestamp =
        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
            .withZone(ZoneId.systemDefault())
            .parse("2000-01-01 00:00:" + ts, Instant::from);
    ChangeMessage cm =
        ChangeMessage.create(key, accountId.orElse(null), timestamp, null, message, null, tag);
    return cm;
  }

  /** Return the change message from the list of messages that has specific message text */
  private static ChangeMessage getChangeMessage(List<ChangeMessage> messages, String messageText) {
    return messages.stream().filter(m -> m.getMessage().equals(messageText)).collect(onlyElement());
  }

  /** Return the comment from the list of comments that has specific message text */
  private CommentInfo getComment(List<CommentInfo> comments, String messageText) {
    return comments.stream().filter(c -> c.message.equals(messageText)).collect(onlyElement());
  }
}
